{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "9b1f6811",
   "metadata": {},
   "outputs": [],
   "source": [
    "import tkinter as tk\n",
    "from tkinter import ttk, messagebox, filedialog\n",
    "from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg\n",
    "from matplotlib.figure import Figure\n",
    "import matplotlib.pyplot as plt\n",
    "import datetime\n",
    "import json"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7dc09bdb",
   "metadata": {},
   "outputs": [],
   "source": [
    "class InteractivePlot:\n",
    "    def __init__(self, root, max_window=40):\n",
    "        self.root = root\n",
    "        self.root.title(\"✏️ 手绘折线图工具\")\n",
    "        self.max_window = max_window  # 固定显示最近N个点\n",
    "        # 初始化数据存储（优化默认点间距）\n",
    "        self.points = [(1.1, 1.1), (2.2, 2.2),(3.3, 3.3)]  # 间距0.5（初始范围0-2时占25%宽度）\n",
    "        self.initial_range = [0.0, 2.0]  # 初始坐标轴范围\n",
    "\n",
    "        # 创建画布\n",
    "        self.fig = Figure(figsize=(8, 5), dpi=100)\n",
    "        self.ax = self.fig.add_subplot(111)\n",
    "        self.line, = self.ax.plot([], [], 'bo-', linewidth=2, markersize=6)  # 折线对象\n",
    "        self.line.set_clip_on(False)  # 允许点超出坐标轴范围\n",
    "        self.ax.set_title(\"点击画布添加节点（右键清除）\")\n",
    "        self.ax.set_xlabel(\"X 轴\")\n",
    "        self.ax.set_ylabel(\"Y 轴\")\n",
    "        self.ax.grid(True, linestyle=\"--\", alpha=0.7)\n",
    "        # 初始坐标轴范围调整为更合理的区间\n",
    "        self.ax.set_xlim(*self.initial_range)\n",
    "        self.ax.set_ylim(*self.initial_range)\n",
    "\n",
    "        # 嵌入Tkinter\n",
    "        self.canvas = FigureCanvasTkAgg(self.fig, master=root)\n",
    "        self.canvas.get_tk_widget().pack(side=tk.LEFT, fill=tk.BOTH, expand=True)\n",
    "\n",
    "        # 绑定事件\n",
    "        self.canvas.mpl_connect(\"button_press_event\", self.on_click)\n",
    "        self.canvas.mpl_connect(\"key_press_event\", self.on_key)\n",
    "\n",
    "        # 创建控制栏\n",
    "        self.control_frame = tk.Frame(root, padx=10, pady=10)\n",
    "        self.control_frame.pack(side=tk.RIGHT, fill=tk.Y)\n",
    "\n",
    "        self.clear_btn = ttk.Button(\n",
    "            self.control_frame, text=\"❌ 清除画布\", command=self.clear_canvas\n",
    "        )\n",
    "        self.clear_btn.pack(pady=5, fill=tk.X)\n",
    "\n",
    "        self.save_btn = ttk.Button(\n",
    "            self.control_frame, text=\"💾 保存数据\", command=self.save_data\n",
    "        )\n",
    "        self.save_btn.pack(pady=5, fill=tk.X)\n",
    "\n",
    "        self.load_btn = ttk.Button(\n",
    "            self.control_frame, text=\"📂 加载数据\", command=self.load_data\n",
    "        )\n",
    "        self.load_btn.pack(pady=5, fill=tk.X)\n",
    "\n",
    "        # 初始化绘图\n",
    "        self.update_plot()\n",
    "\n",
    "    def update_plot(self):\n",
    "        # 维护固定窗口（仅保留最近max_window个点）\n",
    "        if len(self.points) > self.max_window:\n",
    "            self.points = self.points[-self.max_window:]  # 自动截断旧点\n",
    "        \n",
    "        x, y = zip(*self.points) if self.points else ([], [])\n",
    "        if self.points:\n",
    "            self.ax.relim()  # 重新计算数据范围\n",
    "            self.ax.autoscale()  # 自动调整坐标系\n",
    "        # 动态计算坐标轴（仅调整Y轴，X轴固定为窗口宽度）\n",
    "        if len(self.points) >= 2:\n",
    "            # X轴：始终显示从第一个点到最后一个点的范围（模拟时间轴）\n",
    "            x_min, x_max = x[0], x[-1]\n",
    "            self.ax.set_xlim(x_min - 0.1*(x_max-x_min),  # 左侧10%缓冲\n",
    "                            x_max + 0.1*(x_max-x_min))  # 右侧10%缓冲\n",
    "            \n",
    "            # Y轴：当前数据极值±15%缓冲（避免剧烈跳动）\n",
    "            y_min, y_max = min(y), max(y)\n",
    "            y_buffer = 0.15 * (y_max - y_min) if y_max != y_min else 0.5\n",
    "            self.ax.set_ylim(y_min - y_buffer, y_max + y_buffer)\n",
    "        else:\n",
    "            # 单个点或初始两点时使用固定范围\n",
    "            self.ax.set_xlim(0, 2)\n",
    "            self.ax.set_ylim(0, 2)\n",
    "        \n",
    "        # 仅更新数据，不清空画布（关键优化）\n",
    "        self.line.set_data(x, y)\n",
    "        self.ax.set_title(f\"点击添加节点（当前{len(self.points)}个点，窗口{self.max_window}）\")\n",
    "        self.canvas.draw()\n",
    "    def constrain_to_initial_range(self, x, y):\n",
    "        \"\"\"前两点强制在初始范围[0,2]\"\"\"\n",
    "        return (max(0, min(x, 2)), max(0, min(y, 2)))\n",
    "\n",
    "    def find_closest_point(self, click_x, click_y):\n",
    "        \"\"\"返回最近点的索引（距离<10像素视为有效）\"\"\"\n",
    "        closest_idx = -1\n",
    "        min_dist_sq = 100  # 10像素阈值\n",
    "        for idx, (x, y) in enumerate(self.points):\n",
    "            px, py = self.ax.transData.transform((x, y))\n",
    "            dist_sq = (click_x - px)**2 + (click_y - py)**2\n",
    "            if dist_sq < min_dist_sq:\n",
    "                min_dist_sq = dist_sq\n",
    "                closest_idx = idx\n",
    "        return closest_idx if min_dist_sq < 100 else -1\n",
    "    def on_click(self, event):\n",
    "        if event.button == 1 and event.inaxes:\n",
    "            if len(self.points) < 2:  # 前两点为基准，强制在初始范围\n",
    "                x, y = self.constrain_to_initial_range(event.xdata, event.ydata)\n",
    "                self.points.append((x, y))\n",
    "            else:\n",
    "                # 智能插入：找到最近点，插入到其后（或末尾）\n",
    "                closest_idx = self.find_closest_point(event.x, event.y)\n",
    "                if closest_idx != -1:\n",
    "                    # 在最近点后插入（实现折线修正）\n",
    "                    self.points.insert(closest_idx + 1, (event.xdata, event.ydata))\n",
    "                else:\n",
    "                    # 空白处点击，追加到末尾\n",
    "                    self.points.append((event.xdata, event.ydata))\n",
    "            self.update_plot()\n",
    "        \n",
    "        elif event.button == 3:  # 右键智能删除\n",
    "            closest_idx = self.find_closest_point(event.x, event.y)\n",
    "            if closest_idx != -1:\n",
    "                del self.points[closest_idx]\n",
    "                # 少于2个点时恢复初始状态\n",
    "                if len(self.points) < 2:\n",
    "                    self.points = self.initial_points.copy()\n",
    "                self.update_plot()\n",
    "            else:\n",
    "                self.points = self.initial_points.copy()  # 右键无点时重置\n",
    "                self.update_plot()\n",
    "    # 保留原有控制按钮创建、保存加载等逻辑（见后文说明）\n",
    "    def create_control_buttons(self):\n",
    "        self.control_frame = tk.Frame(root, padx=10, pady=10)\n",
    "        self.control_frame.pack(side=tk.RIGHT, fill=tk.Y)\n",
    "        \n",
    "        self.clear_btn = ttk.Button(\n",
    "            self.control_frame, text=\"❌ 清除画布\", command=lambda: self.reset_plot()\n",
    "        )\n",
    "        self.clear_btn.pack(pady=5, fill=tk.X)\n",
    "\n",
    "    def reset_plot(self):\n",
    "        self.points = self.initial_points.copy()  # 重置为初始两点\n",
    "        self.update_plot()\n",
    "    def find_closest_point(self, click_x, click_y):\n",
    "        closest_idx = -1\n",
    "        min_distance = float('inf')\n",
    "        for idx, (x, y) in enumerate(self.points):\n",
    "            x_pixel, y_pixel = self.ax.transData.transform((x, y))\n",
    "            distance = (click_x - x_pixel)**2 + (click_y - y_pixel)**2\n",
    "            if distance < min_distance:\n",
    "                min_distance = distance\n",
    "                closest_idx = idx\n",
    "        return closest_idx if min_distance < 100 else -1\n",
    "\n",
    "    def clear_canvas(self):\n",
    "        self.points = []\n",
    "        # self.ax.clear()\n",
    "        self.ax.set_title(\"点击画布添加节点（右键清除）\")\n",
    "        self.ax.grid(True, linestyle=\"--\", alpha=0.7)\n",
    "        self.canvas.draw()\n",
    "        self.update_plot()  # 触发更新\n",
    "        \n",
    "    def save_data(self):\n",
    "        if not self.points:\n",
    "            messagebox.showwarning(\"提示\", \"请先绘制折线图\")\n",
    "            return\n",
    "\n",
    "        try:\n",
    "            x_min, x_max = self.ax.get_xlim()\n",
    "            y_min, y_max = self.ax.get_ylim()\n",
    "\n",
    "            normalized_points = []\n",
    "            for x, y in self.points:\n",
    "                nx = (x - x_min) / (x_max - x_min) if (x_max - x_min) != 0 else 0\n",
    "                ny = (y - y_min) / (y_max - y_min) if (y_max - y_min) != 0 else 0\n",
    "                normalized_points.append({\"x\": round(nx, 6), \"y\": round(ny, 6)})\n",
    "\n",
    "            data = {\n",
    "                \"metadata\": {\n",
    "                    \"created_at\": datetime.datetime.now().isoformat(),\n",
    "                    \"x_range\": [x_min, x_max],\n",
    "                    \"y_range\": [y_min, y_max],\n",
    "                    \"points_count\": len(self.points)\n",
    "                },\n",
    "                \"coordinates\": normalized_points\n",
    "            }\n",
    "\n",
    "            timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
    "            filename = f\"handdrawn_data_{timestamp}.json\"\n",
    "\n",
    "            with open(filename, \"w\", encoding=\"utf-8\") as f:\n",
    "                json.dump(data, f, ensure_ascii=False, indent=4)\n",
    "\n",
    "            messagebox.showinfo(\"成功\", f\"数据已保存为：\\n{filename}\")\n",
    "        except Exception as e:\n",
    "            messagebox.showerror(\"错误\", f\"保存失败：{str(e)}\")\n",
    "\n",
    "    def load_data(self):\n",
    "        try:\n",
    "            filename = filedialog.askopenfilename(\n",
    "                title=\"选择数据文件\",\n",
    "                filetypes=[(\"JSON文件\", \"*.json\"), (\"所有文件\", \"*.*\")]\n",
    "            )\n",
    "            if not filename:\n",
    "                return\n",
    "\n",
    "            with open(filename, \"r\", encoding=\"utf-8\") as f:\n",
    "                data = json.load(f)\n",
    "                coords = data.get(\"coordinates\", [])\n",
    "                saved_x_range = data[\"metadata\"][\"x_range\"]\n",
    "                saved_y_range = data[\"metadata\"][\"y_range\"]\n",
    "\n",
    "            self.clear_canvas()\n",
    "            self.ax.set_xlim(saved_x_range[0], saved_x_range[1])\n",
    "            self.ax.set_ylim(saved_y_range[0], saved_y_range[1])\n",
    "            self.ax.relim()  # 重新计算数据范围\n",
    "            self.ax.autoscale()  # 自动调整坐标系\n",
    "            \n",
    "            self.points = []\n",
    "            for p in coords:\n",
    "                x = saved_x_range[0] + p[\"x\"] * (saved_x_range[1] - saved_x_range[0])\n",
    "                y = saved_y_range[0] + p[\"y\"] * (saved_y_range[1] - saved_y_range[0])\n",
    "                self.points.append((x, y))\n",
    "\n",
    "            self.update_plot()\n",
    "            messagebox.showinfo(\"成功\", f\"已加载 {len(self.points)} 个坐标点\")\n",
    "\n",
    "        except Exception as e:\n",
    "            messagebox.showerror(\"加载失败\", f\"{str(e)}\")\n",
    "\n",
    "    def on_key(self, event):\n",
    "        if event.key == \"backspace\" or event.key == \"delete\":\n",
    "            self.clear_canvas()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "id": "dc89dc2a",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 36724 (\\N{CJK UNIFIED IDEOGRAPH-8F74}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 28857 (\\N{CJK UNIFIED IDEOGRAPH-70B9}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 20987 (\\N{CJK UNIFIED IDEOGRAPH-51FB}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 28155 (\\N{CJK UNIFIED IDEOGRAPH-6DFB}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 21152 (\\N{CJK UNIFIED IDEOGRAPH-52A0}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 33410 (\\N{CJK UNIFIED IDEOGRAPH-8282}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 65288 (\\N{FULLWIDTH LEFT PARENTHESIS}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 24403 (\\N{CJK UNIFIED IDEOGRAPH-5F53}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 21069 (\\N{CJK UNIFIED IDEOGRAPH-524D}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 20010 (\\N{CJK UNIFIED IDEOGRAPH-4E2A}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 65292 (\\N{FULLWIDTH COMMA}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 31383 (\\N{CJK UNIFIED IDEOGRAPH-7A97}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 21475 (\\N{CJK UNIFIED IDEOGRAPH-53E3}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:81: UserWarning: Glyph 65289 (\\N{FULLWIDTH RIGHT PARENTHESIS}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 28857 (\\N{CJK UNIFIED IDEOGRAPH-70B9}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 20987 (\\N{CJK UNIFIED IDEOGRAPH-51FB}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 30011 (\\N{CJK UNIFIED IDEOGRAPH-753B}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 24067 (\\N{CJK UNIFIED IDEOGRAPH-5E03}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 28155 (\\N{CJK UNIFIED IDEOGRAPH-6DFB}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 21152 (\\N{CJK UNIFIED IDEOGRAPH-52A0}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 33410 (\\N{CJK UNIFIED IDEOGRAPH-8282}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 65288 (\\N{FULLWIDTH LEFT PARENTHESIS}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 21491 (\\N{CJK UNIFIED IDEOGRAPH-53F3}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 38190 (\\N{CJK UNIFIED IDEOGRAPH-952E}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 28165 (\\N{CJK UNIFIED IDEOGRAPH-6E05}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 38500 (\\N{CJK UNIFIED IDEOGRAPH-9664}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 65289 (\\N{FULLWIDTH RIGHT PARENTHESIS}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n",
      "C:\\Users\\Henan\\AppData\\Local\\Temp\\ipykernel_20200\\806055352.py:134: UserWarning: Glyph 36724 (\\N{CJK UNIFIED IDEOGRAPH-8F74}) missing from font(s) DejaVu Sans.\n",
      "  self.canvas.draw()\n"
     ]
    }
   ],
   "source": [
    "if __name__ == \"__main__\":\n",
    "    root = tk.Tk()\n",
    "    root.geometry(\"1000x600\")\n",
    "    app = InteractivePlot(root)\n",
    "    root.mainloop()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ec45d33",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "918b2725",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "my_env",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
